#!/usr/bin/env pythonw

# Main Python imports
import sys
import os
import webbrowser

# GTK imports
try:
    import pygtk
    if not sys.platform == 'win32':
        pygtk.require('2.0')
    import gtk
    import gtk.glade
except:
    sys.stderr.write("ERROR: The GTK+ toolkit and PyGTK"
                     " extension are required to run this program.\n")
    sys.exit(1)

# Project imports
import workflow

# Exception imports
from yaml import YAMLError, MarkedYAMLError
from zipimport import ZipImportError
from workflow import CWWorkflowError

VERSION = "1.0"

class MainWindow(object):
    """
    The main class of CryptoWorkflow. Controls the graphical component and
    provides the layout for the program.
    """
    def __init__(self):
        # Import the Glade layout
        self.widgetLayout = gtk.glade.XML("mainLayout.glade")
        # Grab the necessary objects from the Glade layout
        self.window = self.widgetLayout.get_widget("MainWindow")
        self.inputsDisplay = self.widgetLayout.get_widget("workflowInputs")
        self.componentsDisplay = \
                        self.widgetLayout.get_widget("workflowComponents")
        self.outputsDisplay = self.widgetLayout.get_widget("workflowOutputs")
        self.title = self.widgetLayout.get_widget("workflowTitle")
        self.description = self.widgetLayout.get_widget("workflowDescription")
        self.statusBar = self.widgetLayout.get_widget("statusBar")
        # Make list of Glade objects to hide when no workflow is loaded
        self.displayElements = [
                                self.description,
                                self.widgetLayout.get_widget("inputsLabel"),
                                self.widgetLayout.get_widget("workflowLabel"),
                                self.widgetLayout.get_widget("labelBox"),
                                self.widgetLayout.get_widget("topHSeperator")
                                ]
        for i in self.displayElements:
            i.hide()
        # Connect the GUI event listeners
        connectionList = {
                          "on_fileItemQuit_activate": gtk.main_quit,
                          "on_mainWindow_destroy": gtk.main_quit,
                          "on_fileItemOpen_activate": self.openWorkflow,
                          "on_helpItemAbout_activate": self.aboutCW,
                          "on_helpItemHelp_activate": self.showHelp,
                          "on_toolbarOpen_clicked": self.openWorkflow,
                          "on_toolbarReset_clicked": self.resetWorkflow,
                          "on_toolbarStep_clicked": self.stepWorkflow,
                          "on_toolbarRun_clicked": self.runWorkflow
                          }
        self.widgetLayout.signal_autoconnect(connectionList)
        
    def openWorkflow(self, widget):
        """
        Method to prompt the user for workflow to load and to create all
        necessary objects representing the workflow in the GUI.
        """
        # Get user input
        open_dialog = gtk.FileChooserDialog(title="Open Workflow", 
                                            parent=self.window, 
                                            action=gtk.FILE_CHOOSER_ACTION_OPEN,
                                            buttons=(gtk.STOCK_CANCEL,
                                                     gtk.RESPONSE_CANCEL,
                                                     gtk.STOCK_OPEN,
                                                     gtk.RESPONSE_OK)
                                            )
        open_dialog.set_default_response(gtk.RESPONSE_OK)
        open_dialog.set_current_folder(os.path.abspath(os.curdir) + \
                                       "/workflows")
        filter = gtk.FileFilter()
        filter.set_name("Workflow Files")
        # Make sure we can only open .cwf files
        filter.add_pattern("*.cwf")
        open_dialog.add_filter(filter)
        response = open_dialog.run()
        # If we get an "OK" response, start the workflow with that data
        if response == gtk.RESPONSE_OK:
            try:
                self.resetWorkflow(None)
                self.workflow = workflow.Workflow(open_dialog.get_filename())
                self.showWorkflow()
            except CWWorkflowError:
                message = "The file could not be loaded. There is" + \
                                " an error in the file format."
                error_box = gtk.MessageDialog(parent=None,
                                              type=gtk.MESSAGE_ERROR, 
                                              buttons=gtk.BUTTONS_CLOSE,
                                              message_format=message)
                error_box.run()
                error_box.destroy()
        open_dialog.destroy()
    
    def aboutCW(self, widget):
        """
        Method to display the program's About dialog.
        """
        about_cryptoworkflow = gtk.AboutDialog()
        about_cryptoworkflow.set_name("CryptoWorkflow")
        about_cryptoworkflow.set_version(VERSION)
        about_cryptoworkflow.set_copyright("Copyright (C) 2008 Craig Radcliffe")
        about_cryptoworkflow.set_license("".join(open("copying.txt").readlines()))
        about_cryptoworkflow.set_website("http://cryptoworkflow.googlecode.com/")
        about_cryptoworkflow.set_website_label("CryptoWorkflow Website")
        about_cryptoworkflow.set_authors(["Craig Radcliffe"])
        about_cryptoworkflow.set_program_name("CryptoWorkflow")
        about_cryptoworkflow.run()
        about_cryptoworkflow.destroy()
    
    def showHelp(self, widget):
        """
        Method to display the program's Help file.
        """
        webbrowser.open_new_tab("http://code.google.com/p/cryptoworkflow/wiki/Instructions")
    
    def resetWorkflow(self, widget):
        """
        Method to reset the currently-loaded workflow.
        """
        # Delete the current workflow iterators, if any
        try:
            del(self.__workflowSteps)
            del(self.__workflowDisplayIterator)
        except AttributeError:
            pass
        else:
            # Clear all input fields
            for input in self.inputs:
                self.inputs[input].entry.set_text("")
            for step in self.workflowStepsDisplay:
                for output in step.components:
                    step.components[output].entry.set_text("")
                    step.predictions[output].reset()
                step.drag_unhighlight()
    
    def stepWorkflow(self, widget):
        """
        A method to move one step forward in the workflow.
        """
        # Only execute if a workflow has been loaded
        if hasattr(self, "workflow"):
            try:
                self.__workflowDisplayIterator.next()
            # Create workflow if not already created
            except AttributeError:
                try:
                    self.__workflowDisplayIterator = self.workflowDisplayIterator()
                    self.__workflowDisplayIterator.next()
                # Display error if there are problems with the input
                except ValueError, (message):
                    self.inputError(str(message))
                except StopIteration:
                    del(self.__workflowDisplayIterator)
                except CWWorkflowError:
                    self.resetWorkflow(None)
                    try:
                        del(self.__workflowDisplayIterator)
                    except AttributeError:
                        pass
                    message = "The file could not be loaded. There is" + \
                                    " an error in the file format."
                    error_box = gtk.MessageDialog(parent=None,
                                                  type=gtk.MESSAGE_ERROR, 
                                                  buttons=gtk.BUTTONS_CLOSE,
                                                  message_format=message)
                    error_box.run()
                    error_box.destroy()
                    
            # Do nothing if there are no more steps
            except StopIteration:
                del(self.__workflowDisplayIterator)
    
    def runWorkflow(self, widget):
        """
        A method to run the entire workflow.
        """
        # Only execute if a workflow has been loaded
        if hasattr(self, "workflow"):
            try:
                try:
                    self.__workflowDisplayIterator.next()
                # Create workflow if not already created
                except AttributeError:
                    self.__workflowDisplayIterator = self.workflowDisplayIterator()
                except ValueError, (message):
                    self.inputError(str(message))
                except CWWorkflowError:
                    self.resetWorkflow(None)
                    try:
                        del(self.__workflowDisplayIterator)
                    except AttributeError:
                        pass
                    message = "The file could not be loaded. There is" + \
                                    " an error in the file format."
                    error_box = gtk.MessageDialog(parent=None,
                                                  type=gtk.MESSAGE_ERROR, 
                                                  buttons=gtk.BUTTONS_CLOSE,
                                                  message_format=message)
                    error_box.run()
                    error_box.destroy()
            # Display error if there are problems with the input
            except ValueError, (message):
                self.inputError(str(message))
            # Stop if end of iteration reached
            except StopIteration:
                del(self.__workflowDisplayIterator)
            # Otherwise, loop through remainder of workflow
            else:
                try:
                    for i in self.__workflowDisplayIterator:
                        pass
                except ValueError, (message):
                    self.inputError(str(message))
                except CWWorkflowError:
                    self.resetWorkflow(None)
                    try:
                        del(self.__workflowDisplayIterator)
                    except AttributeError:
                        pass
                    message = "The file could not be loaded. There is" + \
                                    " an error in the file format."
                    error_box = gtk.MessageDialog(parent=None,
                                                  type=gtk.MESSAGE_ERROR, 
                                                  buttons=gtk.BUTTONS_CLOSE,
                                                  message_format=message)
                    error_box.run()
                    error_box.destroy()
    
    def workflowDisplayIterator(self):
        """
        A generator that provides an iterator over the evaluation and display 
        of the steps in the workflow.
        """
        input_values = {}
        # Grab inputs
        try:
            for k in self.workflow.inputs:
                input_values[k] = self.inputs[k].entry.get_text()
            # Initialize the workflow steps generator
            self.__workflowSteps = self.workflow.steps(input_values)
            for step in self.workflowStepsDisplay:
                # Evaluate the step
                results = self.__workflowSteps.next()
                # Display the result and check against user prediction
                for i,j in results.items():
                    if step.predictions[i].entry.get_text() == j:
                        step.predictions[i].correct()
                    else:
                        step.predictions[i].incorrect()
                    step.components[i].entry.set_text(j)
                # Draw a box around the current workflow step
                step.drag_highlight()
                # Pass back to the calling object
                yield True
                step.drag_unhighlight()
        except AttributeError:
            yield False
    
    def showWorkflow(self):
        """
        Method to load the graphical components in the workflow.
        """
        for i in self.displayElements:
            i.show()
        self.title.set_label("<big><b>"+self.workflow.title+"</b></big>")
        self.description.set_label(self.workflow.description)
        self.initializeInputs()
        self.initializeComponents()
            
    def initializeInputs(self):
        """
        Method to load the input components of the workflow GUI.
        """
        # Delete inputs if they exists
        try:
            for i in self.inputs:
                self.inputs[i].destroy()
        except AttributeError:
            pass
        self.inputs = {}
        for input in self.workflow.inputs:
            self.inputs[input] = LabelEntry(input)
            if self.workflow.sizes[input]:
                self.inputs[input].label.set_label(input + " (<i>" + \
                                             str(self.workflow.sizes[input]) + \
                                             " " + self.workflow.unit + \
                                             "</i>)")
            self.inputsDisplay.pack_start(self.inputs[input])
            self.inputs[input].show()

    def initializeComponents(self):
        """
        Method to load the operation components of the workflow GUI.
        """
        # Delete the steps if they exist
        try:
            for i in self.workflowStepsDisplay:
                i.hsep.destroy()
                i.destroy()
        except AttributeError:
            pass
        # Delete the labels if they exist
        try:
            for i in self.workflowStepsDisplayLabels:
                i.destroy()
        except AttributeError:
            pass
        self.workflowStepsDisplay = []
        self.workflowStepsDisplayLabels = []
        if self.workflow.operations is not None:
            for operation in self.workflow.operations:
                # Display operation name
                label = gtk.Label()
                label.set_use_markup(True)
                label.set_label("<big>" + \
                                self.workflow.operationNames[operation['name']] + \
                                "</big>")
                self.workflowStepsDisplayLabels.append(label)
                # Display prediction and output fields
                self.workflowStepsDisplay.append(
                                        GraphicalWorkflowStep(operation['outputs']))
            for i in xrange(len(self.workflowStepsDisplay)):
                # Pack and show the components
                self.componentsDisplay.pack_start(self.workflowStepsDisplayLabels[i],
                                                  padding=5)
                self.workflowStepsDisplayLabels[i].show()
                self.componentsDisplay.pack_start(self.workflowStepsDisplay[i])
                self.workflowStepsDisplay[i].show()
                # Insert horizontal separator to indicate end of step
                self.workflowStepsDisplay[i].hsep = gtk.HSeparator()
                self.componentsDisplay.pack_start(self.workflowStepsDisplay[i].hsep)
                self.workflowStepsDisplay[i].hsep.show()
            
    def inputError(self, message):
        """
        Method to display error box and delete the workflow display iterator 
        upon error in the user-entered input.
        """
        error_box = gtk.MessageDialog(parent=None, 
                                      type=gtk.MESSAGE_ERROR, 
                                      buttons=gtk.BUTTONS_CLOSE,
                                      message_format=message)
        del(self.__workflowDisplayIterator)
        error_box.run()
        error_box.destroy()

        
class LabelEntry(gtk.HBox):
    """
    A class to construct an Entry box with a Label.
    """
    def __init__(self, title, expand=True, fill=True, padding=5, editable=True, xalign=0):
        super(LabelEntry, self).__init__()
        self._fill = fill
        self._padding = padding
        self._expand = expand
        self.label = gtk.Label(title.capitalize() + ":")
        self.label.set_use_markup(True)
        self.label.set_width_chars(20)
        self.label.set_alignment(1.0, 0.0)
        self.entry = gtk.Entry()
        self.entry.set_editable(editable)
    
    def show(self):
        """
        Show the elements in the LabelEntry
        """
        self.pack_start(self.label, expand=False, fill=self._fill, padding=self._padding)
        self.pack_start(self.entry, expand=self._expand, fill=self._fill, padding=self._padding)
        self.label.show()
        self.entry.show()
        super(LabelEntry, self).show()

        
class PredictionLabelEntry(LabelEntry):
    """
    A class to construct a LabelEntry box with extra elements for the prediction box
    """
    def __init__(self, title, expand=True, fill=True, padding=5, editable=True, xalign=0):
        # Set feedback icons
        self.__correct = gtk.Image()
        self.__correct.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
        self.__incorrect = gtk.Image()
        self.__incorrect.set_from_stock(gtk.STOCK_CANCEL, gtk.ICON_SIZE_BUTTON)
        self.__notProcessed = gtk.Image()
        self.__notProcessed.set_from_stock(gtk.STOCK_EXECUTE, gtk.ICON_SIZE_BUTTON)
        super(PredictionLabelEntry, self).__init__(title, expand, fill,
                                                   padding, editable, xalign)
    def show(self):
        """
        Show the icons and elements in the LabelEntry
        """
        self.pack_start(self.__correct)
        self.pack_start(self.__incorrect)
        self.pack_start(self.__notProcessed)
        self.__notProcessed.show()
        super(PredictionLabelEntry, self).show()
        
    def correct(self):
        """
        Method to indicate a correct prediction.
        """
        self.__notProcessed.hide()
        self.__correct.show()
        self.entry.modify_base(gtk.STATE_NORMAL, gtk.gdk.color_parse("#aaffaa"))
    
    def incorrect(self):
        """
        Method to indicate an incorrect prediction.
        """
        self.__notProcessed.hide()
        self.__incorrect.show()
        self.entry.modify_base(gtk.STATE_NORMAL, gtk.gdk.color_parse("#ffaaaa"))
        
    def reset(self):
        """
        Reset the prediction field
        """
        self.__incorrect.hide()
        self.__correct.hide()
        self.entry.set_text("")
        self.entry.modify_base(gtk.STATE_NORMAL, gtk.gdk.color_parse("#aaaaff"))
        self.__notProcessed.show()

        
class GraphicalWorkflowStep(gtk.HBox):
    """
    A class to construct a graphical view of a workflow step, including prediction
    fields.
    """
    def __init__(self, step_outputs, expand=True, fill=True, padding=5):
        super(GraphicalWorkflowStep, self).__init__()
        self.__predictions = {}
        self.__components = {}
        self.__predictionsDisplay = gtk.VBox()
        self.__componentsDisplay = gtk.VBox()
        self.pack_start(self.__predictionsDisplay, expand=expand, 
                        fill=True, padding=padding)
        self.pack_start(self.__componentsDisplay, expand=expand, 
                        fill=True, padding=padding)
        for output in step_outputs:
            self.__predictions[output] = PredictionLabelEntry(output, xalign=0.24)
            self.__predictions[output].entry.modify_base(gtk.STATE_NORMAL, 
                                                       gtk.gdk.color_parse("#aaaaff"))
            self.__predictionsDisplay.pack_start(self.__predictions[output],
                                                 expand=expand,
                                                 fill=False,
                                                 padding=padding)
            self.__components[output] = LabelEntry(output, editable=False)
            self.__componentsDisplay.pack_start(self.__components[output], 
                                                expand=expand,
                                                fill=False, 
                                                padding=padding)
    
    @property
    def predictions(self):
        return self.__predictions
    @property
    def components(self):
        return self.__components
    
    def show(self):
        for i in self.__predictions:
            self.__predictions[i].show()
        for i in self.__components:
            self.__components[i].show()
        self.__predictionsDisplay.show()
        self.__componentsDisplay.show()
        super(GraphicalWorkflowStep, self).show()
        

def main(*args, **kwargs):
    """
    The main program function. Called when file is executed directly.
    """
    
    gtk.gdk.threads_init()
    gtk.gdk.threads_enter()
    
    cwapp = MainWindow()
    gtk.main()
    
    gtk.gdk.threads_leave()

# Execute main if file executed directly.
if __name__ == "__main__":
    main()